﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using PostSharp.CodeModel;
using PostSharp.Collections;
using MicroContainer;

namespace MicroContainer.CompileTime.Generators
{
   internal class BuildMethodCodeGenerator : ICodeGenerator
   {
      private readonly TypeMappingDef _mappingDef;

      public BuildMethodCodeGenerator(TypeMappingDef mappingDef)
      {
         _mappingDef = mappingDef;
      }

      public void Generate(CodeGenerationContext ctx)
      {
         GenerateCreateMethod(ctx);
         if (_mappingDef.Mode == InstantiationMode.Singleton)
         {
            GenerateSingletonBuildMethod(ctx);
            GenerateSingleInstanceInitCode(ctx);
         }
      }

      private void GenerateSingletonBuildMethod(CodeGenerationContext ctx)
      {         
         string suffix = _mappingDef.UniqieId.ToString().Replace("-", "_");
         MethodDefDeclaration buildMethod = ctx.ContainerType.Methods.GetOneByName("Build_" + suffix);
         FieldDefDeclaration singleInstanceField = ctx.ContainerType.Fields.GetByName("_singleInstance_" + suffix);
         InstructionBlock block = buildMethod.MethodBody.RootInstructionBlock;
         InstructionWriter writer = new InstructionWriter();
         InstructionSequence seq = buildMethod.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seq, NodePosition.Before, null);
         writer.AttachInstructionSequence(seq);
         writer.EmitInstruction(OpCodeNumber.Ldarg_0);
         writer.EmitInstructionField(OpCodeNumber.Ldfld, singleInstanceField);
         writer.EmitInstruction(OpCodeNumber.Ret);
         writer.DetachInstructionSequence();
      }

      private void GenerateSingleInstanceInitCode(CodeGenerationContext ctx)
      {
         MethodDefDeclaration ctor = ctx.ContainerType.Methods.GetOneByName(".ctor");
         string suffix = _mappingDef.UniqieId.ToString().Replace("-", "_");
         MethodDefDeclaration createMethod = ctx.ContainerType.Methods.GetOneByName("Create_" + suffix);
         FieldDefDeclaration singleInstanceField = ctx.ContainerType.Fields.GetByName("_singleInstance_" + suffix);

         InstructionBlock block = ctor.MethodBody.RootInstructionBlock;
         InstructionWriter writer = new InstructionWriter();
         InstructionSequence seq = ctor.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seq, NodePosition.After, null);
         writer.AttachInstructionSequence(seq);
                  
         writer.EmitInstruction(OpCodeNumber.Ldarg_0);
         writer.EmitInstruction(OpCodeNumber.Ldarg_0);
         writer.EmitInstructionMethod(OpCodeNumber.Call, createMethod);
         writer.EmitInstructionField(OpCodeNumber.Stfld, singleInstanceField);

         writer.DetachInstructionSequence();
      }

      private void GenerateCreateMethod(CodeGenerationContext ctx)
      {
         TypeDefDeclaration substType =
            ctx.ContainerType.Module.FindType(_mappingDef.From, BindingOptions.Default).            
            GetTypeDefinition();            
         MethodDefDeclaration mappingsGetter = ctx.ContainerType.Methods.GetOneByName((_mappingDef.Mode == InstantiationMode.Singleton ? "Create_" : "Build_") + _mappingDef.UniqieId.ToString().Replace("-", "_"));         

         TypeDefDeclaration typeBeingBuilt;
         MethodDefDeclaration constructionMethod;

         if (_mappingDef.To != null)
         {
            typeBeingBuilt =
               ctx.ContainerType.Module.FindType(_mappingDef.To, BindingOptions.Default).GetTypeDefinition();
            constructionMethod = FindBestConstructor(typeBeingBuilt, ctx);
            if (constructionMethod == null)
            {
               throw new FatalErrorException("MC002", _mappingDef.To.FullName);
            }
         }
         else
         {
            constructionMethod = ctx.ContainerType.Module.FindMethod(_mappingDef.FactoryMethod.Method,
                                                                     BindingOptions.Default).GetMethodDefinition();
            typeBeingBuilt = constructionMethod.ReturnParameter.ParameterType.GetTypeDefinition();
         }
         InstructionBlock block = mappingsGetter.MethodBody.RootInstructionBlock;
         block.DefineLocalVariable(typeBeingBuilt.Translate(ctx.ContainerType.Module), "tmp");
         block.DefineLocalVariable(substType.Translate(ctx.ContainerType.Module), "result");
         InstructionWriter writer = new InstructionWriter();
         InstructionSequence seq = mappingsGetter.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seq, NodePosition.Before, null);
         writer.AttachInstructionSequence(seq);
         EmitConstructorCall(constructionMethod, writer, ctx);
         writer.EmitInstruction(OpCodeNumber.Stloc_0);

         foreach (PropertyDeclaration prop in typeBeingBuilt.Properties)
         {
            EmitPropertyResolution(prop, block, writer, ctx);
         }

         writer.EmitInstruction(OpCodeNumber.Ldloc_0);
         writer.EmitInstruction(OpCodeNumber.Stloc_1);
         writer.EmitInstruction(OpCodeNumber.Ldloc_1);
         writer.EmitInstruction(OpCodeNumber.Ret);         
         writer.DetachInstructionSequence();
      }


      private void EmitPropertyResolution(PropertyDeclaration prop, InstructionBlock block, InstructionWriter writer, CodeGenerationContext ctx)
      {
         PropertyInfo reflectionWrapper = prop.GetReflectionWrapper(null, null);
         DependencyAttribute attr = reflectionWrapper.GetCustomAttributes<DependencyAttribute>(true).FirstOrDefault();
         MethodDefDeclaration setterMethod = prop.Members.GetBySemantic(MethodSemantics.Setter).Method;
         if ( ((setterMethod.Attributes & MethodAttributes.Static) == 0) && prop.CanWrite)
         {
            writer.EmitInstruction(OpCodeNumber.Ldloc_0);            
            string mappingName = attr != null ? attr.MappingName : null;
            TypeMappingDef propertyMappingDef;
            if (ctx.TypeMappings.TryGetValue(new TypeMappingKey(reflectionWrapper.PropertyType, mappingName),
                                             out propertyMappingDef))
            {
               MethodDefDeclaration argumentBuilder =
                  ctx.ContainerType.Methods.GetOneByName("Build_" +
                                                         propertyMappingDef.UniqieId.ToString().Replace("-", "_"));
               writer.EmitInstruction(OpCodeNumber.Ldarg_0);
               writer.EmitInstructionMethod(OpCodeNumber.Call, argumentBuilder);                                 
            }
            else if (attr == null || !attr.Required)
            {
               writer.EmitInstruction(OpCodeNumber.Ldnull);               
            }
            else
            {
               throw new FatalErrorException("MC003", prop.Name, _mappingDef.To.FullName);
            }            
            writer.EmitInstructionMethod(OpCodeNumber.Callvirt, setterMethod);            
         }
         else if (!prop.CanWrite)
         {
            throw new FatalErrorException("MC005", prop.Name, _mappingDef.To.FullName);
         }
         else if (attr != null)
         {
            throw new FatalErrorException("MC004", prop.Name, _mappingDef.To.FullName);
         }
      }

      private void EmitConstructorCall(MethodDefDeclaration constructionMethod, InstructionWriter writer, CodeGenerationContext ctx)
      {
         bool autoinjection = constructionMethod.Equals(ctx.ConfigurationMethod);
         if (autoinjection)
         {
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
         }
         else
         {
            foreach (ParameterDeclaration param in constructionMethod.Parameters)
            {
               ParameterInfo reflectionWrapper = param.GetReflectionWrapper(null, null);
               DependencyAttribute attr =
                  reflectionWrapper.GetCustomAttributes<DependencyAttribute>(true).FirstOrDefault();
               string mappingName = attr != null ? attr.MappingName : null;
               TypeMappingDef argumentMappingDef;
               if (ctx.TypeMappings.TryGetValue(new TypeMappingKey(reflectionWrapper.ParameterType, mappingName),
                                                     out argumentMappingDef))
               {
                  MethodDefDeclaration argumentBuilder =
                     ctx.ContainerType.Methods.GetOneByName("Build_" +
                                                            argumentMappingDef.UniqieId.ToString().Replace("-", "_"));
                  writer.EmitInstruction(OpCodeNumber.Ldarg_0);
                  writer.EmitInstructionMethod(OpCodeNumber.Call, argumentBuilder);
               }
               else
               {
                  writer.EmitInstruction(OpCodeNumber.Ldnull);
               }
            }
            if (constructionMethod.Name == ".ctor")
            {
               writer.EmitInstructionMethod(OpCodeNumber.Newobj, constructionMethod);
            }
            else
            {
               writer.EmitInstructionMethod(OpCodeNumber.Call, constructionMethod);
            }
         }
      }

      private MethodDefDeclaration FindBestConstructor(TypeDefDeclaration typeBeingBuilt, CodeGenerationContext ctx)
      {
         int maxResolvedParams = -1;
         MethodDefDeclaration bestConstructor = null;
         IEnumerable<MethodDefDeclaration> constructors = typeBeingBuilt.Methods.GetByName(".ctor");
         //ConstructorInfo[] constructors = _mappingDef.To.GetConstructors();
         foreach (MethodDefDeclaration ctor in constructors)
         {
            bool isUsable = true;
            int resolvedParams = 0;
            foreach (ParameterDeclaration param in ctor.Parameters)
            {
               string mappingName = null;
               bool mandatory = false;

               ParameterInfo reflectionWrapper = param.GetReflectionWrapper(null, null);
               DependencyAttribute attr = reflectionWrapper.GetCustomAttributes<DependencyAttribute>(true).FirstOrDefault();
               if (attr != null)
               {
                  mappingName = attr.MappingName;
                  mandatory = attr.Required;
               }
               TypeMappingKey key = new TypeMappingKey(reflectionWrapper.ParameterType, mappingName);
               if (ctx.TypeMappings.ContainsKey(key))
               {
                  resolvedParams++;
               }
               else if (mandatory)
               {
                  isUsable = false;
                  break;
               }
            }
            if (isUsable && resolvedParams > maxResolvedParams)
            {
               maxResolvedParams = resolvedParams;
               bestConstructor = ctor;
            }
         }
         return bestConstructor;
      }
   }
}
